package com.github.facadefs.sftp;

import com.github.facadefs.FileView;
import com.github.facadefs.Selector;
import com.github.facadefs.support.SessionBasedFileSystem;
import com.github.facadefs.util.Paths;
import com.google.common.collect.Lists;
import com.sshtools.j2ssh.SshClient;
import com.sshtools.j2ssh.SshException;
import com.sshtools.j2ssh.authentication.AuthenticationProtocolState;
import com.sshtools.j2ssh.authentication.PasswordAuthenticationClient;
import com.sshtools.j2ssh.authentication.PublicKeyAuthenticationClient;
import com.sshtools.j2ssh.authentication.SshAuthenticationClient;
import com.sshtools.j2ssh.sftp.*;
import com.sshtools.j2ssh.transport.IgnoreHostKeyVerification;
import com.sshtools.j2ssh.transport.publickey.SshPrivateKey;
import com.sshtools.j2ssh.transport.publickey.SshPrivateKeyFile;
import lombok.Getter;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.util.List;
import java.util.Vector;

/**
 * 基于 sshtools:j2ssh-core 实现的 SFTP 文件系统.
 * <p/>
 * sshtools:j2ssh-core:0.29 登录 OSX 10.9 ssh 无法认证, Linux 暂时没有发现问题
 * 建议更新使用 com.sshtools:j2ssh-maverick
 *
 * @author vacoor
 * @see JschSftpFileSystem
 * @see J2sshMaverickSftpFileSystem
 * @since 1.0
 * @deprecated 算法支持不全
 */
@Slf4j
@Getter
@Setter
@Deprecated
public class J2sshSftpFileSystem extends SessionBasedFileSystem<J2sshSftpFileSystem.SftpChannel> {
    private static final String NO_SUCH_FILE_MSG = "No such file";

    static class SftpChannel extends SftpSubsystemClient {
        private final SshClient ssh;

        private SftpChannel(final SshClient ssh) {
            this.ssh = ssh;
        }
    }

    private String host;
    private int port = 22;
    private String username;
    private String password;
    private byte[] privateKey;
    private byte[] passphrase;

    private String chroot;

    /**
     * {@inheritDoc}
     */
    @Override
    protected SftpChannel connect() throws IOException {
        final SshClient ssh = connectToServer();
        try {
            final SftpChannel sftp = new SftpChannel(ssh);
            if (!ssh.openChannel(sftp)) {
                throw new SshException("The SFTP subsystem failed to start");
            } else if (!sftp.initialize()) {
                throw new SshException("The SFTP Subsystem could not be initialized");
            }
            return sftp;
        } catch (final IOException ioe) {
            disconnect(ssh);
            throw ioe;
        }
    }

    private SshClient connectToServer() throws IOException {
        final SshClient ssh = new SshClient();
        ssh.connect(host, port, new IgnoreHostKeyVerification());

        SshAuthenticationClient client;
        if (null != privateKey && privateKey.length > 0) {
            final String passphraseStr = null != passphrase ? new String(passphrase, StandardCharsets.UTF_8) : null;
            final SshPrivateKey privateKeyToUse = SshPrivateKeyFile.parse(privateKey).toPrivateKey(passphraseStr);
            final PublicKeyAuthenticationClient pkc = new PublicKeyAuthenticationClient();
            pkc.setUsername(username);
            pkc.setKey(privateKeyToUse);
            client = pkc;
        } else {
            final PasswordAuthenticationClient pac = new PasswordAuthenticationClient();
            pac.setUsername(username);
            pac.setPassword(password);
            client = pac;
        }

        final int code = ssh.authenticate(client);
        if (AuthenticationProtocolState.COMPLETE != code) {
            throw new IOException(String.format("Login failed on server - %s, port - %s", host, port));
        }
        return ssh;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void disconnect(final SftpChannel sftp) {
        if (null == sftp) {
            return;
        }
        try {
            try {
                if (!sftp.isClosed()) {
                    sftp.close();
                }
            } finally {
                disconnect(sftp.ssh);
            }
        } catch (final IOException e) {
            log.warn("Server - {}:{} disconnect error: {}.", host, port, e.getMessage());
        }
    }

    private void disconnect(final SshClient ssh) {
        if (null != ssh && ssh.isConnected()) {
            ssh.disconnect();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected FileView stat(final SftpChannel sftp, final String path) throws IOException {
        final String absPath = toAbsolutePath(path);
        try {
            final FileAttributes stat = stat0(sftp, absPath);
            return null != stat ? toView(sftp, absPath, null, stat) : null;
        } catch (final IOException ignore) {
            return null;
        }
    }

    private FileAttributes stat0(final SftpChannel sftp, final String absPath) throws IOException {
        try {
            return sftp.getAttributes(absPath);
        } catch (final IOException ex) {
            if (NO_SUCH_FILE_MSG.equals(ex.getMessage())) {
                return null;
            }
            throw ex;
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected FileView[] ls(final SftpChannel sftp, final String path) throws IOException {
        final String absPath = toAbsolutePath(path);
        final FileAttributes stat = stat0(sftp, path);
        if (null == stat) {
            return new FileView[0];
        }
        if (!stat.isDirectory()) {
            return new FileView[]{this.toView(sftp, absPath, null, stat)};
        }

        final List<SftpFile> children = listChildren0(sftp, absPath);
        final List<FileView> views = Lists.newArrayListWithExpectedSize(children.size());
        for (final SftpFile child : children) {
            final String filename = child.getFilename();
            if (Paths.isCurrentDirName(filename) || Paths.isParentDirName(filename)) {
                // child.close();
                continue;
            }
            views.add(toView(sftp, toAbsolutePath(absPath, filename), child.getLongname(), child.getAttributes()));
            // child.close();
        }
        return views.toArray(new FileView[0]);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void ls(final SftpChannel sftp, final String path, final Selector selector) throws IOException {
        final String absPath = toAbsolutePath(path);
        final FileAttributes stat = stat0(sftp, path);
        if (null == stat) {
            return;
        }
        if (!stat.isDirectory()) {
            selector.select(this.toView(sftp, absPath, null, stat));
            return;
        }

        final List<SftpFile> children = listChildren0(sftp, absPath);
        for (final SftpFile child : children) {
            final String filename = child.getFilename();
            if (Paths.isCurrentDirName(filename) || Paths.isParentDirName(filename)) {
                continue;
            }
            final FileView v = this.toView(sftp, toAbsolutePath(absPath, filename), child.getLongname(), stat);
            if (Selector.CONTINUE != selector.select(v)) {
                break;
            }
        }
    }

    private List<SftpFile> listChildren0(final SftpChannel sftp, final String absPath) throws IOException {
        final Vector<SftpFile> children = new Vector<SftpFile>();
        final SftpFile directory = sftp.openDirectory(absPath);
        sftp.listChildren(directory, children);
        directory.close();
        return children;
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected boolean mkdirs(final SftpChannel sftp, final String path) throws IOException {
        final String absPath = toAbsolutePath(path);
//        try {
        final FileAttributes stat = stat0(sftp, path);
        if (null != stat && !stat.isDirectory()) {
            throw new IOException("File already exists and is not directory: " + path);
        }

        sftp.recurseMakeDirectory(absPath);
        return true;
//        } catch (final IOException ex) {
//            return false;
//        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void rm(final SftpChannel sftp, final String path, final boolean recurse) throws IOException {
        final String absPath = toAbsolutePath(path);
        final FileAttributes stat = stat0(sftp, absPath);
        if (null == stat) {
            return;
        }
        if (stat.isDirectory()) {
            final List<SftpFile> children = listChildren0(sftp, absPath);
            if (!recurse && !children.isEmpty()) {
                throw new IOException("You cannot delete non-empty directory, use recurse=true to override");
            }
            for (final SftpFile child : children) {
                final String filename = child.getFilename();
                if (Paths.isCurrentDirName(filename) || Paths.isParentDirName(filename)) {
                    // child.close();
                    continue;
                }
                this.rm(sftp, toAbsolutePath(absPath, filename), recurse);
                // child.close();
            }
            sftp.removeDirectory(absPath);
        } else {
            sftp.removeFile(absPath);
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void rename(final SftpChannel sftp, final String oldPath, final String newPath) throws IOException {
        final String absOldPath = toAbsolutePath(oldPath);
        final String absNewPath = toAbsolutePath(newPath);

        // FIXME 可能是目录重命名
        sftp.renameFile(absOldPath, absNewPath);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected InputStream open(final SftpChannel sftp, final String path) throws IOException {
        final String absPath = toAbsolutePath(path);
        final FileAttributes stat = stat0(sftp, absPath);
        if (null == stat) {
            return null;
        }
        if (stat.isDirectory()) {
            throw new IOException("Can't open " + path + " because it is a directory");
        }
        return new SftpFileInputStream(sftp.openFile(absPath, SftpSubsystemClient.OPEN_READ));
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected OutputStream write(final SftpChannel sftp, final String path, final String contentType, final boolean override) throws IOException {
        final String absPath = toAbsolutePath(path);
        final FileAttributes stat = stat0(sftp, absPath);
        if (stat != null) {
            if (!override) {
                throw new IOException("File already exists: " + absPath);
            } else if (stat.isDirectory()) {
                throw new IOException("Can't override " + path + " because it is a directory");
            }
        } else {
            final int i = absPath.lastIndexOf(Paths.PATH_SEPARATOR);
            final String absParentPath = i <= 0 ? null : absPath.substring(0, i);
            if (null != absParentPath && !this.mkdirs(sftp, absParentPath)) {
                throw new IOException("write(): Mkdirs failed to create: " + absParentPath);
            }
        }
        return new SftpFileOutputStream(sftp.openFile(
                absPath, SftpSubsystemClient.OPEN_CREATE | SftpSubsystemClient.OPEN_WRITE | SftpSubsystemClient.OPEN_TRUNCATE
        ));
    }

    private String toAbsolutePath(final String path) {
        return toAbsolutePath(chroot, path);
    }

    private String toAbsolutePath(final String base, final String path) {
        return Paths.toAbsolutePath(base, path);
    }

    private String toRelativePath(final String path) {
        return Paths.toRelativePath(chroot, path);
    }

    private FileView toView(SftpSubsystemClient sftp, String absPath, String longname, FileAttributes attrs) throws IOException {
        final long length = attrs.getSize().longValue();
        final boolean dir = attrs.isDirectory();
        final long lastModifiedTime = attrs.getModifiedTime().longValue() * 1000;
        final long lastAccessTime = attrs.getAccessedTime().longValue() * 1000;
        final int permissions = attrs.getPermissions().intValue();
        final int uid = attrs.getUID().intValue();
        final int gid = attrs.getGID().intValue();
        final boolean isLink = attrs.isLink();
        String symlink = null;

        if (isLink) {
            try {
                symlink = sftp.getSymbolicLinkTarget(absPath);
            } catch (final Exception ex) {
                symlink = null;
            }
        }

        String[] info = Utils.parseUserAndGroup(longname);
        String owner = 1 < info.length ? info[0] : String.valueOf(uid);
        String group = 1 < info.length ? info[1] : String.valueOf(gid);

        return new FileView(toRelativePath(absPath), length, dir, lastModifiedTime, lastAccessTime, permissions, owner, group, symlink);
    }

}
